package org.afk.sisy;

import java.io.*;
import java.nio.file.*;
import java.sql.*;
import java.util.concurrent.*;

import org.afk.sisy.db.*;
import org.afk.sisy.io.*;
import org.afk.tools.com.*;
import org.afk.tools.io.*;
import org.afk.tools.log.*;

public class Sisy implements SisyProgressListener
{
	private static final String MATCH = "match";
	private static final String UNMATCHED_SECONDARY = "unmatchedSecondary";
	private static final String UNMATCHED_PRIMARY = "unmatchedPrimary";
	private static final String SCAN_SECONDARY = "scanSecondary";
	private static final String MD5 = "md5";
	private static final String HASH = "hash";
	private static final String DB = "db";
	private static final String ATTIC_DIR = "atticDir";
	private static final String TO = "to";
	private static final String FROM = "from";
	private static final String ATTIC = "attic";
	private static final String SYNC = "sync";
	private static final String COMPARE = "compare";

	private static Path createPath(final String path)
	{
		if (path == null)
			return null;
		return Paths.get(path);
	}

	private static void exitSyntax(final SisyJob job)
	{
		System.out.println(syntax(job, null));
		System.exit(0);
	}

	private static String fallback(final String string, final String string2)
	{
		if (string != null)
			return string;
		return string2;
	}

	private static ErrorHandler initErrorHandler(final boolean verbose)
	{
		final ConsoleErrorHandler consoleErrorHandler = new ConsoleErrorHandler();
		consoleErrorHandler.setDebug(verbose);
		return consoleErrorHandler;
	}

	private static String syntax(final SisyJob job, final String error)
	{
		final StringBuilder builder = new StringBuilder();
		if (error != null)
			builder.append(error).append("\r\n\r\n");

		builder.append("-" + FROM + " <dir>\r\n");
		builder.append(" -" + TO + " <dir>\r\n");
		builder.append(" [-" + UNMATCHED_PRIMARY + " <*" + UnmatchedOption.COPY + "|" + UnmatchedOption.DELETE + "|" + UnmatchedOption.IGNORE + ">]\r\n");
		builder.append(" [-" + SCAN_SECONDARY + " [-" + UNMATCHED_SECONDARY + " <" + UnmatchedOption.COPY + "|" + UnmatchedOption.DELETE + "|*"
				+ UnmatchedOption.IGNORE + ">]]\r\n");
		builder.append(" [-" + MATCH + " <*" + MatchOption.COMPARE + "|" + MatchOption.IGNORE + "|" + MatchOption.SYNC + ">]\r\n");
		builder.append(" [-" + COMPARE + " <*" + CompareOption.ATTRIBUTES + "|" + CompareOption.HASH + "|" + CompareOption.DB + "> [-" + HASH
				+ " <algorythm>] ]\r\n");
		builder.append(" [-" + SYNC + " <*" + SyncOption.NEWER + "|" + SyncOption.BIGGER + "|" + SyncOption.PRIMARY + "|" + SyncOption.SECONDARY + "|"
				+ SyncOption.BOTH + ">]\r\n");
		builder.append(" [-" + ATTIC + " <*" + AtticOption.RENAME + "|" + AtticOption.MOVE + "|" + AtticOption.DELETE + "> [-" + ATTIC_DIR
				+ " <dir>]]   // move -> dir\r\n");
		builder.append(" [-" + DB + " <file>]\r\n");
		builder.append(" [-v] [-?] [-h] [-help]\r\n");
		return builder.toString();
	}

	public static SisyJob createJob(final Arguments arguments)
	{
		final SisyJob job = new SisyJob();
		job.setPrimaryDir(createPath(arguments.get(FROM)));
		job.setDataBase(createPath(arguments.get(DB)));
		job.setSecondaryDir(createPath(arguments.get(TO)));
		job.setAtticDir(createPath(arguments.get(ATTIC_DIR)));
		job.setAlgorythm(fallback(arguments.get(HASH), MD5));

		setMatchOption(job, arguments.get(MATCH));
		setAtticOption(job, arguments.get(ATTIC));
		job.setScanSecondaryDir(arguments.hasArgument(SCAN_SECONDARY));
		if (job.isScanSecondaryDir())
			setUnmatchedSecondaryOption(job, arguments.get(UNMATCHED_SECONDARY));
		setUnmatchedPrimaryOption(job, arguments.get(UNMATCHED_PRIMARY));
		setSyncOption(job, arguments.get(SYNC));
		setCompareOption(job, arguments.get(COMPARE));
		return job;
	}

	private static void setCompareOption(final SisyJob job, final String value)
	{
		if (value != null)
		{
			final CompareOption option = CompareOption.valueOf(value);
			if (option != null)
				job.setCompareOption(option);
			else
				syntax(job, "Unknown CompareOption " + value);
		}
	}

	private static void setMatchOption(final SisyJob job, final String value)
	{
		if (value != null)
		{
			final MatchOption option = MatchOption.valueOf(value);
			if (option != null)
				job.setMatchOption(option);
			else
				syntax(job, "Unknown MatchOption " + value);
		}
	}

	private static void setSyncOption(final SisyJob job, final String value)
	{
		if (value != null)
		{
			final SyncOption option = SyncOption.valueOf(value);
			if (option != null)
				job.setSyncOption(option);
			else
				syntax(job, "Unknown SyncOption " + value);
		}
	}

	private static void setUnmatchedPrimaryOption(final SisyJob job, final String value)
	{
		if (value != null)
		{
			final UnmatchedOption option = UnmatchedOption.valueOf(value);
			if (option != null)
				job.setUnmatchedPrimaryOption(option);
			else
				syntax(job, "Unknown UnmatchedOption " + value);
		}
	}

	private static void setUnmatchedSecondaryOption(final SisyJob job, final String value)
	{
		if (value != null)
		{
			final UnmatchedOption option = UnmatchedOption.valueOf(value);
			if (option != null)
				job.setUnmatchedSecondaryOption(option);
			else
				syntax(job, "Unknown UnmatchedOption " + value);
		}
	}

	private static void setAtticOption(final SisyJob job, final String value)
	{
		if (value != null)
		{
			final AtticOption option = AtticOption.valueOf(value);
			if (option != null)
				job.setAtticOption(option);
			else
				syntax(job, "Unknown AtticOption " + value);
		}
	}

	public static void main(final String[] args)
	{
		final Arguments arguments = new Arguments(args);
		final SisyJob job = createJob(arguments);
		if (arguments.hasArgument("h") || arguments.hasArgument("help") || arguments.hasArgument("?"))
			exitSyntax(job);
		final boolean verbose = arguments.hasArgument("v");
		final ErrorHandler errorHandler = initErrorHandler(verbose);
		errorHandler.debug("" + arguments);

		final Sisy app = new Sisy(errorHandler, job, verbose);
		if ((job.getPrimaryDir() != null) && (job.getSecondaryDir() != null))
			try
			{
				app.sync();
			}
			catch (final Throwable e)
			{
				errorHandler.warning("while syncing", e);
			}
		else
			exitSyntax(job);
	}

	final SisyProgress progress = new SisyProgress();
	private final ErrorHandler errorHandler;
	private final SisyJob job;
	private Long lastProgress;
	private final boolean verbose;

	public Sisy(final ErrorHandler errorHandler, final SisyJob job, final boolean verbose)
	{
		this.job = job;
		this.errorHandler = errorHandler;
		this.verbose = verbose;
	}

	private void closeDb(final PathDataBase fileDataBase)
	{
		if (fileDataBase != null)
			try
			{
				fileDataBase.close();
			}
			catch (final SQLException e)
			{
				this.errorHandler.warning("while closing DB", e);
			}
	}

	private SisyMatcher createDataBaseMatcher(final SisyProgress progress2, final PathHasher fileHasher, final SisyCompareResultProcessor processor)
	{
		final SisyMatcher fileMatcher = new SisyMatcher(this.job, this.errorHandler, fileHasher, this.progress, processor);
		return fileMatcher;
	}

	private SisyCompareResultProcessor createProcessor()
	{
		final SisyCompareResultProcessor processor = new SisyCompareResultProcessor(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()), this.errorHandler);
		return processor;
	}

	private PathDataBase prepareDataBase()
	{
		if (this.job.getDataBase() == null)
			return null;
		try
		{
			Class.forName("org.h2.Driver");
			final PathDataBase fileDataBase = new PathDataBase(this.job);
			fileDataBase.open();
			fileDataBase.prepareScan();
			return fileDataBase;
		}
		catch (final SQLException e)
		{
			this.errorHandler.warning("while preparing Database", e);
			return null;
		}
		catch (final ClassNotFoundException e)
		{
			this.errorHandler.warning("H2 DB jar missing", e);
			return null;
		}
	}

	private void syncDirs(final SisyProgress progress, final PathHasher fileHasher, final SisyCompareResultProcessor processor) throws InterruptedException
	{
		final PathDataBase fileDataBase = prepareDataBase();
		try
		{
			if (fileDataBase != null)
				this.errorHandler.info("Using data base at " + this.job.getDataBase());
			final SisyMatcher sisyMatcher = createDataBaseMatcher(progress, fileHasher, processor);
			sisyMatcher.setPathDataBase(fileDataBase);

			this.errorHandler.info("Initialized stuff");
			sisyMatcher.matchFiles();
			this.errorHandler.info("Matched files");

			processor.waitForEnd();
		}
		finally
		{
			closeDb(fileDataBase);
		}
		this.errorHandler.info("Done");
	}

	@Override
	public void progress()
	{
		if (this.verbose && ((this.lastProgress == null) || ((System.currentTimeMillis() - this.lastProgress) > 100)))
		{
			this.errorHandler.info("" + this.progress);
			this.lastProgress = System.currentTimeMillis();
		}
	}

	public void sync() throws InterruptedException, IOException
	{
		final StopWatch stopWatch = new StopWatch();
		this.progress.addListener(this);
		final PathHasher fileHasher = new PathHasher(this.errorHandler, this.job.getAlgorythm());
		final SisyCompareResultProcessor processor = createProcessor();
		processor.start();
		syncDirs(this.progress, fileHasher, processor);

		this.errorHandler.info("After " + stopWatch + " " + this.progress);
	}
}
